package cn.ljy.common.exception.handler;


import cn.ljy.common.exception.model.*;
import cn.ljy.common.model.result.ResponseData;
import cn.ljy.common.model.result.ResultCode;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.*;

/**
 * @author ljy
 * 20190819创建
 * 20220115最后修改
 * 用于处理本服务端所有异常信息并返回前端
 * 说明：使用此异常类，应按照把业务层异常往控制层抛，并且在控制层不轻易做异常处理，都交由此类处理
 */
@Slf4j
//@ControllerAdvice
//@ResponseBody
@RestControllerAdvice
@Order(100)//如果其他module有异常处理类，则应设计在100以下的数值
public class MyExceptionHandler {

    /**
     * 处理自定义异常：表单/前端异常
     */
    @ExceptionHandler
    public ResponseData MyWebException(MyWebException e) {
        log.info("前端异常：{}", e.getMessage());
        return ResponseData.error(e.getMessage());
    }

    /**
     * 处理自定义异常：处理业务异常
     */
    @ExceptionHandler
    public ResponseData myServiceException(MyServiceException e) {
        log.info("业务异常：{}", e.getMessage());
        return ResponseData.error(e.getMessage());
    }

    /**
     * 处理自定义表格导入异常
     * 说明：返回的异常信息为集合类的json字符串。需要重新将json转换为对象集合再传递到前端
     */
    @ExceptionHandler
    public ResponseData myExcelException(MyExcelException e) {
        log.info("表格导入异常：{}", e.getMessage());
        return ResponseData.error("表格导入失败",JSON.parse(e.getMessage()));
    }

    /**
     * 处理自定义异常：文件上传或下载异常
     */
    @ExceptionHandler
    public ResponseData myFileException(MyFileException e) {
        log.info("文件操作异常：{}", e.getMessage());
        return ResponseData.error(e.getMessage());
    }

    /**
     * 处理自定义异常：token异常
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler
    public ResponseData myTokenException(MyTokenException e) {
        log.info("token异常：{}", e.getMessage());
        return ResponseData.error(ResultCode.UNAUTHORIZED.getCode(),e.getMessage());
    }

    /**
     * 处理自定义异常：被拒绝访问异常
     */
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler
    public ResponseData myForbiddenException(MyForbiddenException e) {
        log.info("拒绝访问异常：{}", e.getMessage());
        return ResponseData.error(ResultCode.FORBIDDEN.getCode(),e.getMessage());
    }

    /**
     * 处理自定义异常：需要确认的异常。例如部门时，部门下还有其他账户信息。
     * 注意：此处拼接不同，因为要确认的异常通常会返回错误信息集合，因此这里统一进行拼接；如果不需要此处拼接，可以在业务层拼接好再调用
     * 说明：只进行简单的拼接（一般替换2个对象），复杂的请自行拼接
     */
    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
    @ExceptionHandler
    public ResponseData MyConfirmException(MyConfirmException e) {
        log.info("操作需要确认的异常：{}", e.getMessage());
        String errorMessageResult;//定义最终要返回的错误信息
        if (e.getErrorMessages() != null && e.getErrorMessages().size() > 0) {//如果传递这个就表示需要拼接
            //定义错误信息
            StringBuilder errorMessage = new StringBuilder();
            int index = 0;//索引
            errorMessage.append("删除失败：");
            //循环处理错误信息
            for (String name : e.getErrorMessages().keySet()) {
                //e.getMessage()传递的应该是前面的字符串：String.format("删除失败：库位%s正被%d种物料占用，无法删除！",seat.getName(),materialInventoryDetailNumber)
                //即删除失败：库位%s正被%d种物料占用，无法删除！
                errorMessage.append(String.format(e.getMessage(), name, e.getErrorMessages().get(name)));
                if (index == e.getErrorMessages().size() - 1) {//最后一个不进行处理，是为了交给前端来决定最后一句话的符号结尾。
//                    errorMessage.append("。");
                } else {
                    errorMessage.append("，");
                }
                index++;
            }
            errorMessageResult = errorMessage.toString();
        }else{
            errorMessageResult=e.getMessage();
        }
        return ResponseData.error(HttpStatus.NOT_ACCEPTABLE.value(), errorMessageResult);
    }

    /**
     * 处理权限异常
     */
    /*@ExceptionHandler
    public ResponseData MyLoginException(MyCustomerLoginException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append("操作失败:");
        strBuilder.append(e.getMessage());
        map.put(Constant.JSON_MESSAGE, strBuilder.toString());
        map.put(Constant.JSON_CODE, JsonCode.LOGIN.getValue());//返回错误信息
        log.info("未登录", e.getMessage());
        return map;
    }*/

    //处理文件上传信息
    @ExceptionHandler
    public ResponseData handleMultipartException(MaxUploadSizeExceededException e) {
        // 这里可以编写返回给客户端的错误信息
        return ResponseData.error(HttpStatus.PAYLOAD_TOO_LARGE.value(),"上传文件超出服务器规定的大小");
    }


    /**
     * web的restful响应异常
     * 错误信息提示（例子）：restful请求不符合接口规范：服务端支持的接口类型是POST但传递的类型是GET
     */
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseData requestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        String methord=e.getMethod();//前端请求的restful类型，如put、post等
        String[] supportedMethods=e.getSupportedMethods();//获取服务端支持的restful类型，如put、post等
        StringBuilder stringBuilder=new StringBuilder();
        stringBuilder.append("服务端支持的接口类型是");
        for (int i = 0; i < supportedMethods.length; i++) {
            String supportedMethod=supportedMethods[i];
            stringBuilder.append(supportedMethod);
            if(i<supportedMethods.length-1){
                stringBuilder.append("|");
            }
        }
        stringBuilder.append("但传递的类型是"+methord);
        String error=stringBuilder.toString();
        //未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
        log.info("restful请求不符合接口规范：{}", error);
        return ResponseData.error(ResultCode.METHODNOTALLOW.getCode(),"restful请求不符合接口规范："+ error);
    }

    /**
     * spring mvc在接收参数时，如果类型转换错误则会弹出此异常
     * 例如：某控制器的某个请求如下
     * public ResponseData hello(Integer number1, BigDecimal number2){
     * }
     * 如果传递过来的number1为a，则会出现下述异常
     */
    @ExceptionHandler
    public ResponseData methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        String fieldName=e.getName();//出错的字段名
        Object fieldValue=e.getValue();//出错字段的值
        Class type=e.getRequiredType();//出错字段的类型
        String error="类型转换错误：fieldName的值为fieldValue，无法转换为type类型"
                .replace("fieldName",fieldName)
                .replace("fieldValue",fieldValue.toString())
                .replace("type",type.getName());
        log.info("spring mvc参数类型转换异常：{}", error);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),error);
    }

    /**
     * json转换异常时出现
     * 例1：
     * @PostMapping("/testPost")
     * public ResponseData jsonTest(@Valid @RequestBody User user){
     * 如果没有传递user对象，则会出现该异常
     */
    @ExceptionHandler
    public ResponseData httpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.info("json转换异常：{}", e.getMessage());
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),"json转换异常："+ e.getMessage());
    }


    /**
     * @author ljy
     * 20220115
     * spring自定义验证框架异常
     * 说明：本异常只针对@valid修饰的(但是不包括和@)对象中属性验证的异常
     * 例1：典型触发该异常的函数
     * @GetMapping
     * public ResponseData hello(Integer number1, @Valid User user)
     * 并且在User类在中设置了
     * public class User implements java.io.Serializable {
     *     @NotBlank(message = "账户名不能为空")
     *     private String username;
     * 如果user对象中的username（账户名）为空，那么就会出现该异常
     * 例2：用@Valid @RequestBody同时修饰的参数不会出现该异常，而是ConstraintViolationException异常
     * @PostMapping("/testPost")
     * public ResponseData jsonTest(@Valid @RequestBody User user){
     */
//    @ExceptionHandler
//    public ResponseData bindException(BindException e) {
//        //获取该异常的错误信息返回对象
//        BindingResult bindingResult=e.getBindingResult();
//        StringBuilder strBuilder = new StringBuilder();
//        String className=bindingResult.getObjectName();//出错的参数，按例子来说这里是user
//        strBuilder.append(className+"参数验证不通过，错误原因如下:");
//        List<ObjectError> allErrors = bindingResult.getAllErrors();//获取具体的错误信息，这里会获取user对象中错误参数信息集合
//        for(int i=0;i<allErrors.size();i++){
//            ObjectError error=allErrors.get(i);
//            if(i>0){
//                strBuilder.append(",");
//            }
//            strBuilder.append(error.getDefaultMessage());//错误信息
//        }
//        String errorResult=strBuilder.toString();
//        log.info("spring mvc参数绑定异常：{}", errorResult);
//        return ResponseData.error(ResultCode.BADREQUEST.getCode(),errorResult);
//    }

    /**
     * 用于处理函数中的实体对象中字段的类型转换失败。
     * 主要针对非实体参数，可以针对实体中的多个属性类型转换错误进行统一处理，例如list(SysUserQuery query,Integer page, Integer pageSize)中query参数中的属性转换异常
     * @param e
     * @return
     */
//    @ExceptionHandler
//    public ResponseData handleBindException(BindException e) {
//        //获取所有错误信息
//        List<String> details = e.getFieldErrors().stream()
//                .map(fe -> {
//                    //因为无法直接获取错误字段的类型，因此通过反射来获取
//                    Class<?> type = BeanUtils.getPropertyDescriptor(e.getTarget().getClass(), fe.getField()).getPropertyType();
//                    return String.format("%s的值为%s，无法转化为%s类型",
//                            fe.getField(), fe.getRejectedValue(), type.getSimpleName());
//                })
//                .collect(Collectors.toList());
//        return ResponseData.error("类型转换错误："+String.join("；", details));
//    }

    /**
     * 说明：用于处理函数中的实体对象（不含JSON转换对象）字段的类型转换失败；spring自定义验证框架异常；
     * 不过如果是同一个接口中，一般只能触发一类异常。首先如果类型转换失败会触发异常，触发侯就不会再出发spring验证框架异常。
     * 情况1：主要针对实体参数（特别注意：如果实体是JSON转换的，那么实体属性的类型转换错误不在这里捕获），可以针对实体中的多个属性类型转换错误进行统一处理，例如list(SysUserQuery query,Integer page, Integer pageSize)中query参数中的属性转换异常
     * 情况2：针对@valid修饰的对象中属性验证的异常，如user对象中的username的@NotBlank异常
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    public ResponseData handleAllBindingErrors(BindException e) {
        //定义错误信息集合。因两种异常不会同时出现，分别以两个错误类型来捕获异常。
        List<String> typeErrors   = new ArrayList<>();//类型转换错误
        List<String> validErrors  = new ArrayList<>();//验证失败错误
        //便利异常信息
        for (FieldError fe : e.getBindingResult().getFieldErrors()) {
            if (fe.isBindingFailure()) {//如果是绑定错误，即类型转换失败。
                //因为无法直接获取错误字段的类型，因此通过反射来获取
                Class<?> type = BeanUtils
                        .getPropertyDescriptor(e.getTarget().getClass(), fe.getField())
                        .getPropertyType();
                typeErrors.add(String.format("%s的值为%s，无法转化为%s类型",
                        fe.getField(), fe.getRejectedValue(), type.getSimpleName()));
            } else {
                // 校验失败
                validErrors.add(fe.getDefaultMessage());
//                validErrors.add(String.format("%s的值为%s，不满足条件[%s]",
//                        fe.getField(), fe.getRejectedValue(),fe.getDefaultMessage()));
            }
        }
        if(typeErrors.size()>0){
            return ResponseData.error("表单中存在类型转换错误："+String.join("；", typeErrors));
        }else{
            return ResponseData.error("表单验证失败："+String.join("；", validErrors));
        }
    }


    /**
     * @author ljy
     * 20220115
     * 处理spring 校验框架validation抛出的异常(二种异常之一)
     * 说明：本异常一般是针对@NotBlank或@NotNull等直接捕获的异常，不包括@valid单独修饰对象时的属性验证异常
     * 或者说使用了spring boot验证框架的异常如果不是BindException，基本都会触发该异常
     * 例如：
     * 在业务层接口编写了验证方法
     *  boolean login(@Valid User user, @NotBlank(message = "账户名不能为空3") String username, String password);
     *  那么当username验证不通过时，会出现ConstraintViolationException异常
     *  反之，如果是user出现异常，则不会出现ConstraintViolationException异常，而是BindException异常
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseData constraintViolationException(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> violations = new LinkedHashSet(e.getConstraintViolations());
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append("操作失败：");
        int index=0;//用于set集合中的计数，方便更好的处理输出信息格式
        for (ConstraintViolation<?> violation : violations) {//如果配置hibernate验证（fail_fast）后，这里就只会出现一个异常了，不过暂时保留此处
            if(index>0){
                strBuilder.append(",");
            }
            String msg=violation.getMessage();//出错的信息
            strBuilder.append(msg);
            index++;
            //break;//因为所有验证在前端显示，格式需要假如html和css进行控制，如果前端包括android等不好控制，所以只显示一条
        }
        String result = strBuilder.toString();
        log.info("spring业务层验证异常：{}", result);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),result);
    }

    /**
     * 20220115：目前未发现该异常出现
     * 20220414：该异常在spring mvc层出现
     * 例如：下述情况会发生该异常
     * public ResponseData updateEnabled(@Valid @RequestBody RoleEnabledParam roleEnabledParam){
     * 并且针对RoleEnabledParam属性做了字段验证
     * public class RoleEnabledParam {
     *     @NotEmpty(message = "ids不能为空")
     *     private Integer[] ids; // 角色id集合
     *     @NotNull(message = "enabled不能为空")
     *     private Boolean enabled; // 可用属性
     * }
     * 处理spring 校验框架validation抛出的异常(二种异常之一)：MethodArgumentNotValidException（官方定义），该异常在控制器验证时出现
     */
    @ExceptionHandler(MethodArgumentNotValidException .class)
    public ResponseData methodArgumentNotValidException (MethodArgumentNotValidException e) {
        // 同样是获取BindingResult对象，然后获取其中的错误信息
        // //在配置hibernate验证（fail_fast）后，这里就只会出现一个异常了
        //如果没有，则可能又多个
        BindingResult bindingResult=e.getBindingResult();//如果配置在spring mvc的验证，会出现这个异常信息
        List<ObjectError> errorList=bindingResult.getAllErrors();//获取异常集合
        StringBuilder messageString=new StringBuilder();
        List<String> messageList=new ArrayList<>();
        for(int index=0;index<errorList.size();index++){
            ObjectError error=errorList.get(index);
            if(index!=0){
                messageString.append(";");
            }
            messageList.add(error.getDefaultMessage());
            messageString.append(error.getDefaultMessage());
        }
//        ObjectError error=errorList.get(0);//由于配置里只有一个异常，所以直接取第一个
//        String message=error.getDefaultMessage();//验证失败的错误信息
        String message="验证失败：message".replace("message",messageString.toString());
        log.info("spring的web验证异常：{}", message);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),message,messageList);
    }

    //spring mvc在接收参数时，如果无法接收到值出现此异常（如该值是必须的，或是不符合传输类型要求，导致无法接收到值）
    @ExceptionHandler
    public ResponseData missingServletRequestParameterException(MissingServletRequestParameterException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        String fieldName=e.getParameterName();//出错的字段名
        String type=e.getParameterType();//出错字段的类型
        String error="参数类型不匹配：参数fieldName类型应该为type"
                .replace("fieldName",fieldName)
                .replace("type",type);
        log.info("spring的request参数接收不到：{}", error);
        return ResponseData.error(ResultCode.BADREQUEST.getCode(),error);
    }


    //spring的文件上传大小异常
    /*@ExceptionHandler(MultipartException.class)
    public Map<String, Object> multipartException(MultipartException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        map.put("state", "上传文件超出大小");  //专门为ueditor写的返回信息，如果不需要可以去掉该行
        map.put(Constant.JSON_MESSAGE, "上传文件超出大小");
        map.put(Constant.JSON_CODE, JsonCode.ERROR.getValue());//返回错误信息
        //未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
        log.info("spring上传组件异常", "上传文件超出大小");
        return map;
    }*/


    //数字转换异常
   /* @ExceptionHandler(NumberFormatException.class)
    public Map<String, Object> numberFormatException(NumberFormatException e) {
        Map<String, Object> map=new HashMap<String, Object>();
        map.put(Constant.JSON_MESSAGE, "类型转换错误：无法将指定字符串转化为数字");
        map.put(Constant.JSON_CODE, JsonCode.ERROR.getValue());//返回错误信息
        //未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
        log.info("字符串无法转换为数字的异常", "类型转换错误：无法将指定字符串转化为数字");
        return map;
    }*/




    /**
     * 处理其他异常
     *  未知异常一般是计划外的，需要重点处理，比如记录下日志，或是自动发送错误信息邮件给技术部
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public ResponseData unknownException(Exception e, HttpServletRequest request) {
        if (request!=null) {
            StringBuilder strBuild = new StringBuilder();
            strBuild.append("global catch reqUrl:{}");
            strBuild.append(request.getRequestURI());
            if (HttpMethod.GET.matches(request.getMethod())) {
                strBuild.append(" queryString:");
                strBuild.append(request.getQueryString());
            }
            log.error(strBuild.toString(), e);
            return ResponseData.error(ResultCode.INTERNALSERVERERROR.getCode(), ResultCode.INTERNALSERVERERROR.getMsg()+":"+e.getMessage());
        }else{
            log.error("global catch：{}", e);
            return ResponseData.error(ResultCode.INTERNALSERVERERROR.getCode(), ResultCode.INTERNALSERVERERROR.getMsg()+":"+e.getMessage());
        }
    }
}
